package com.example.minisqlite;

import java.sql.SQLException;
import java.util.ArrayList;
import java.util.List;

/**
 * 极简SQL解析器：支持
 * <ul>
 *     <li>CREATE TABLE table_name (...)</li>
 *     <li>INSERT INTO table_name (...) VALUES (...)</li>
 *     <li>SELECT ... FROM table_name [WHERE column = literal]</li>
 * </ul>
 */
public class SqlParser {

    public Statement parse(String sql) throws SQLException {
        Tokenizer tokenizer = new Tokenizer(sql);
        if (tokenizer.matchKeyword("CREATE")) {
            Statement stmt;
            if (tokenizer.matchKeyword("TABLE")) {
                stmt = parseCreateTable(tokenizer);
            } else if (tokenizer.matchKeyword("INDEX")) {
                stmt = parseCreateIndex(tokenizer);
            } else {
                throw new SQLException("Expected TABLE or INDEX after CREATE");
            }
            tokenizer.expectEnd();
            return stmt;
        } else if (tokenizer.matchKeyword("INSERT")) {
            tokenizer.expectKeyword("INTO");
            Statement stmt = parseInsert(tokenizer);
            tokenizer.expectEnd();
            return stmt;
        } else if (tokenizer.matchKeyword("SELECT")) {
            Statement stmt = parseSelect(tokenizer);
            tokenizer.expectEnd();
            return stmt;
        } else if (tokenizer.matchKeyword("UPDATE")) {
            Statement stmt = parseUpdate(tokenizer);
            tokenizer.expectEnd();
            return stmt;
        } else if (tokenizer.matchKeyword("DELETE")) {
            tokenizer.expectKeyword("FROM");
            Statement stmt = parseDelete(tokenizer);
            tokenizer.expectEnd();
            return stmt;
        }
        throw new SQLException("Unsupported SQL: " + sql);
    }

    private Statement parseCreateTable(Tokenizer tokenizer) throws SQLException {
        String tableName = tokenizer.expectIdentifier();
        tokenizer.expectSymbol("(");
        List<ColumnDefinition> columns = new ArrayList<>();
        do {
            columns.add(parseColumnDefinition(tokenizer));
        } while (tokenizer.matchSymbol(","));
        tokenizer.expectSymbol(")");
        return new CreateTableStatement(tableName, columns);
    }

    private Statement parseCreateIndex(Tokenizer tokenizer) throws SQLException {
        String indexName = tokenizer.expectIdentifier();
        tokenizer.expectKeyword("ON");
        String tableName = tokenizer.expectIdentifier();
        tokenizer.expectSymbol("(");
        String columnName = tokenizer.expectIdentifier();
        tokenizer.expectSymbol(")");
        return new CreateIndexStatement(indexName, tableName, columnName);
    }

    private ColumnDefinition parseColumnDefinition(Tokenizer tokenizer) throws SQLException {
        String columnName = tokenizer.expectIdentifier();
        String typeKeyword = tokenizer.expectIdentifier();
        ColumnType type = ColumnType.fromKeyword(typeKeyword);
        boolean primaryKey = false;
        if (tokenizer.matchKeyword("PRIMARY")) {
            tokenizer.expectKeyword("KEY");
            primaryKey = true;
        }
        return new ColumnDefinition(columnName, type, primaryKey);
    }

    private Statement parseInsert(Tokenizer tokenizer) throws SQLException {
        String tableName = tokenizer.expectIdentifier();
        tokenizer.expectSymbol("(");
        List<String> columns = new ArrayList<>();
        do {
            columns.add(tokenizer.expectIdentifier());
        } while (tokenizer.matchSymbol(","));
        tokenizer.expectSymbol(")");
        tokenizer.expectKeyword("VALUES");
        tokenizer.expectSymbol("(");
        List<Object> values = new ArrayList<>();
        do {
            values.add(parseLiteral(tokenizer));
        } while (tokenizer.matchSymbol(","));
        tokenizer.expectSymbol(")");
        return new InsertStatement(tableName, columns, values);
    }

    private Statement parseSelect(Tokenizer tokenizer) throws SQLException {
        List<String> columns = new ArrayList<>();
        if (tokenizer.matchSymbol("*")) {
            // empty list signals select all
        } else {
            columns.add(tokenizer.expectIdentifier());
            while (tokenizer.matchSymbol(",")) {
                columns.add(tokenizer.expectIdentifier());
            }
        }
        tokenizer.expectKeyword("FROM");
        String tableName = tokenizer.expectIdentifier();
        Engine.Filter filter = null;
        if (tokenizer.matchKeyword("WHERE")) {
            String column = tokenizer.expectIdentifier();
            tokenizer.expectSymbol("=");
            Object value = parseLiteral(tokenizer);
            filter = new Engine.Filter(column, value);
        }
        if (columns.isEmpty()) {
            columns = null;
        }
        return new SelectStatement(tableName, columns, filter);
    }

    private Statement parseUpdate(Tokenizer tokenizer) throws SQLException {
        String tableName = tokenizer.expectIdentifier();
        tokenizer.expectKeyword("SET");
        List<String> columns = new ArrayList<>();
        List<Object> values = new ArrayList<>();
        do {
            columns.add(tokenizer.expectIdentifier());
            tokenizer.expectSymbol("=");
            values.add(parseLiteral(tokenizer));
        } while (tokenizer.matchSymbol(","));
        Engine.Filter filter = null;
        if (tokenizer.matchKeyword("WHERE")) {
            String column = tokenizer.expectIdentifier();
            tokenizer.expectSymbol("=");
            Object value = parseLiteral(tokenizer);
            filter = new Engine.Filter(column, value);
        }
        return new UpdateStatement(tableName, columns, values, filter);
    }

    private Statement parseDelete(Tokenizer tokenizer) throws SQLException {
        String tableName = tokenizer.expectIdentifier();
        Engine.Filter filter = null;
        if (tokenizer.matchKeyword("WHERE")) {
            String column = tokenizer.expectIdentifier();
            tokenizer.expectSymbol("=");
            Object value = parseLiteral(tokenizer);
            filter = new Engine.Filter(column, value);
        }
        return new DeleteStatement(tableName, filter);
    }

    private Object parseLiteral(Tokenizer tokenizer) throws SQLException {
        Token token = tokenizer.peek();
        if (token.type == TokenType.STRING) {
            tokenizer.consume();
            return token.text;
        } else if (token.type == TokenType.NUMBER) {
            tokenizer.consume();
            if (token.text.contains(".") || token.text.contains("e") || token.text.contains("E")) {
                return Double.parseDouble(token.text);
            }
            return Long.parseLong(token.text);
        } else if (token.type == TokenType.SYMBOL && token.text.equals("-")) {
            tokenizer.consume();
            Token number = tokenizer.peek();
            if (number.type != TokenType.NUMBER) {
                throw new SQLException("Expected number after '-'");
            }
            tokenizer.consume();
            if (number.text.contains(".") || number.text.contains("e") || number.text.contains("E")) {
                return -Double.parseDouble(number.text);
            }
            return -Long.parseLong(number.text);
        }
        throw new SQLException("Unsupported literal: " + token.text);
    }

    private enum TokenType {IDENTIFIER, NUMBER, STRING, SYMBOL, EOF}

    private static final class Token {
        final TokenType type;
        final String text;

        Token(TokenType type, String text) {
            this.type = type;
            this.text = text;
        }
    }

    private static final class Tokenizer {
        private final String sql;
        private final List<Token> tokens = new ArrayList<>();
        private int index = 0;

        Tokenizer(String sql) throws SQLException {
            this.sql = sql;
            tokenize();
        }

        private void tokenize() throws SQLException {
            int length = sql.length();
            int pos = 0;
            while (pos < length) {
                char c = sql.charAt(pos);
                if (Character.isWhitespace(c)) {
                    pos++;
                    continue;
                }
                if (isIdentifierStart(c)) {
                    int start = pos;
                    pos++;
                    while (pos < length && isIdentifierPart(sql.charAt(pos))) {
                        pos++;
                    }
                    tokens.add(new Token(TokenType.IDENTIFIER, sql.substring(start, pos)));
                    continue;
                }
                if (Character.isDigit(c)) {
                    int start = pos;
                    pos++;
                    boolean hasDot = false;
                    while (pos < length) {
                        char ch = sql.charAt(pos);
                        if (Character.isDigit(ch)) {
                            pos++;
                        } else if (ch == '.' && !hasDot) {
                            hasDot = true;
                            pos++;
                        } else if ((ch == 'e' || ch == 'E')) {
                            pos++;
                            if (pos < length && (sql.charAt(pos) == '+' || sql.charAt(pos) == '-')) {
                                pos++;
                            }
                        } else {
                            break;
                        }
                    }
                    tokens.add(new Token(TokenType.NUMBER, sql.substring(start, pos)));
                    continue;
                }
                if (c == '\'') {
                    int start = pos + 1;
                    pos++;
                    StringBuilder sb = new StringBuilder();
                    boolean closed = false;
                    while (pos < length) {
                        char ch = sql.charAt(pos);
                        if (ch == '\'') {
                            if (pos + 1 < length && sql.charAt(pos + 1) == '\'') {
                                sb.append('\'');
                                pos += 2;
                            } else {
                                closed = true;
                                pos++;
                                break;
                            }
                        } else {
                            sb.append(ch);
                            pos++;
                        }
                    }
                    if (!closed) {
                        throw new SQLException("Unterminated string literal");
                    }
                    tokens.add(new Token(TokenType.STRING, sb.toString()));
                    continue;
                }
                if ("(),=*;".indexOf(c) >= 0) {
                    tokens.add(new Token(TokenType.SYMBOL, String.valueOf(c)));
                    pos++;
                    continue;
                }
                if (c == '-') {
                    tokens.add(new Token(TokenType.SYMBOL, "-"));
                    pos++;
                    continue;
                }
                throw new SQLException("Unexpected character: " + c);
            }
            tokens.add(new Token(TokenType.EOF, ""));
        }

        private boolean isIdentifierStart(char c) {
            return Character.isLetter(c) || c == '_';
        }

        private boolean isIdentifierPart(char c) {
            return Character.isLetterOrDigit(c) || c == '_';
        }

        boolean matchKeyword(String keyword) {
            Token token = peek();
            if (token.type == TokenType.IDENTIFIER && token.text.equalsIgnoreCase(keyword)) {
                consume();
                return true;
            }
            return false;
        }

        void expectKeyword(String keyword) throws SQLException {
            if (!matchKeyword(keyword)) {
                throw new SQLException("Expected keyword " + keyword);
            }
        }

        String expectIdentifier() throws SQLException {
            Token token = peek();
            if (token.type != TokenType.IDENTIFIER) {
                throw new SQLException("Expected identifier, got " + token.text);
            }
            consume();
            return token.text;
        }

        boolean matchSymbol(String symbol) {
            Token token = peek();
            if (token.type == TokenType.SYMBOL && token.text.equals(symbol)) {
                consume();
                return true;
            }
            return false;
        }

        void expectSymbol(String symbol) throws SQLException {
            if (!matchSymbol(symbol)) {
                throw new SQLException("Expected symbol " + symbol);
            }
        }

        Token peek() {
            return tokens.get(index);
        }

        Token consume() {
            return tokens.get(index++);
        }

        void expectEnd() throws SQLException {
            if (matchSymbol(";")) {
                // optional semicolon
            }
            if (peek().type != TokenType.EOF) {
                throw new SQLException("Unexpected token: " + peek().text);
            }
        }
    }
}
